package br.com.lm.anotacoes;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Robson Motta
 * Pesquisa todas as classes por critérios e retorna as suas anotacoes
 */
public final class AnnotationScan {
    
   /**
    * @param <T> - Qualificacao para a classe de anotacao
    * @param originSearch - Opcional, determina a origem da pesquisa para as classes com anotacoes
    *        Valores permitidos para pesquisa das classes:
    *        1) META-INF = Busca em META-INF/persistence.xml
    *        2) WEB-INF  = Busca em /WEB-INF/classes (Default se o parametro originSearch estiver vazio)
    *        3) Qualquer outro conteudo assume que originSearch contem o nome de um pacote (package name ou arquivo jar)
    * @param annotationClass - Opcional, se informado identifica uma especifica classe de Anotacao desejada, ex: Entity.Class 
    * @return - Conteudo da pesquisa para os criterios adotados e/ou informados
    * AnnotationResult: representa as classes retornadas na pesquisa com suas anotacoes  
    */
    @SuppressWarnings({"UnusedAssignment", "null"})
    public static <T extends Annotation> List<AnnotationResult> getAnnotation(String originSearch, Class<T> annotationClass) {
        List<AnnotationResult> result = null;
        List<String> classesNameWithPackage = null; 

        if ("META-INF".equalsIgnoreCase(originSearch)) {
            //Le classes do META-INF/persistence.xml
            classesNameWithPackage = ReadClassesUtil.getClassesFromPersistenceXml();
        }
        else
        if (("WEB-INF".equalsIgnoreCase(originSearch)) || originSearch == null || originSearch.trim().equals("")) {
            //Le classe do /WEB-INF/classes
            classesNameWithPackage = ReadClassesUtil.getClassNamesFromWebInfClasses();
        }
        else {
            //Le um Package Name
            classesNameWithPackage = ReadClassesUtil.getClassNamesFromPackage(originSearch);
        }
 
        //Processa classes no retorno da pesquisa	
        if (classesNameWithPackage != null) {
            Map<String, List<AnnotationElement>> mapCtrlPacote = new HashMap<String, List<AnnotationElement>>();
            List<AnnotationElement> annoElements = null;
            //Verefica anotacoes 
            for(String classNameWithPackage: classesNameWithPackage) {
                AnnotationElement annotationElement = getAnnotationsByClass(classNameWithPackage, annotationClass);
                if (annotationElement != null) {
                    //Separa o resultado por pacote
                    String packageName = "Default";
                    if (classNameWithPackage.indexOf(".") >= 0) 
                        packageName = classNameWithPackage.substring(0,classNameWithPackage.lastIndexOf("."));        
                    if (mapCtrlPacote.containsKey(packageName)) 
                        annoElements = mapCtrlPacote.get(packageName);
                    else
                        annoElements = new ArrayList<AnnotationElement>(); 
                    annoElements.add(annotationElement);
                    mapCtrlPacote.put(packageName, annoElements);
                }
            }   
            //Cria classe de retorno
            if (!mapCtrlPacote.isEmpty()) {
                result = new ArrayList<AnnotationResult>(mapCtrlPacote.size());
                for (Iterator<Map.Entry<String, List<AnnotationElement>>> iter = mapCtrlPacote.entrySet().iterator(); iter.hasNext();) {
                    Map.Entry<String, List<AnnotationElement>> me = iter.next();
                    annoElements = me.getValue();
                    Map<String, AnnotationElement> mapClasses = new HashMap<String, AnnotationElement>(annoElements.size());
                    //Gera resultado de anotacao por pacote
                    AnnotationResult annotationResult = new AnnotationResult();
                    annotationResult.setAnnotationClassArg(annotationClass);
                    annotationResult.setOriginSearchArg(originSearch);
                    annotationResult.setPackageClasseAnnotated(me.getKey());
                    for (AnnotationElement annoElement: annoElements) 
                        mapClasses.put(annoElement.getClassNameWithAnnotation(), annoElement);    
                    annotationResult.setAllElements(mapClasses);
                    //Grava retorno
                    result.add(annotationResult);                
                } 
            }
        }                   
        return result;
    }
	
    /*
    * AnnotationElement: representa as anotacoes(uma ou mais) atribuida para uma classe pesquisada
    * Uma anotacao pode ter um ou mais elementos (representada pela classe AnnotationData)
    */
    @SuppressWarnings({"rawtypes", "UnusedAssignment"})
    private static <T extends Annotation> AnnotationElement getAnnotationsByClass(String classNameWithPackage, Class<T> annotationClass) {
        AnnotationElement annotationElement = null;
	Map<String, Set<AnnotationData>> elements = null;
        Map<String, Class> elementsRtrnType;
	try {
            //Obtem dados da classe que foi pesquisada
            Class<?> someClass = Class.forName(classNameWithPackage);
            if (!someClass.isAnnotation()) 
              return null;
		      
            //Informacoes da classe
            annotationElement = new AnnotationElement();
            annotationElement.setPackageClassName(someClass.getPackage().getName());
            annotationElement.setClassNameWithAnnotation(someClass.getSimpleName());

            elements = new HashMap<String, Set<AnnotationData>>();
		      
            //Anotacoes de classe
            Annotation[] annotationsClass = someClass.getAnnotations();
            if( annotationsClass != null ) {
                for (Annotation anno : annotationsClass) {
                    if (annotationClass == null || annotationClass.getSimpleName().equals(anno.annotationType().getSimpleName())) {
                        elementsRtrnType = splitMethodRtrnType(anno.annotationType().getDeclaredMethods());
                        Set<AnnotationData> annotationDatas = getAnnotationDatas(anno.annotationType().getSimpleName(), 
                                                 anno.annotationType().getPackage().getName(), 
                                                 someClass.getSimpleName(),
                                                 someClass.getClass(),
                                                 AnnotationType.Class, 
                                                 splitAnnotationValue(anno.annotationType().getDeclaredMethods(), anno.toString()),
                                                 elementsRtrnType,
                                                 someClass.getPackage().getName(),
                                                 someClass.getSimpleName());
                        if (annotationDatas != null) 
                            elements.put(anno.annotationType().getSimpleName(), annotationDatas);		    		  
		    }		    		  
                }
            }

	    //Anotacao de Campos 
            Field[] fieldsClasse = someClass.getDeclaredFields();
            if( fieldsClasse != null ) {
                for (Field f:fieldsClasse) {
                    Annotation[] annoFields = f.getAnnotations();
                    if (annoFields != null) {
                        for (Annotation annoField:annoFields) {
                            if (annotationClass == null || annotationClass.getSimpleName().equals(annoField.annotationType().getSimpleName())) {
                                elementsRtrnType = splitMethodRtrnType(annoField.annotationType().getDeclaredMethods());
                                Set<AnnotationData> annotationDatas = getAnnotationDatas(annoField.annotationType().getSimpleName(), 
                                                         annoField.annotationType().getPackage().getName(), 
                                                         f.getName(),
                                                         f.getType(),
                                                         AnnotationType.Field, 
                                                         splitAnnotationValue(annoField.annotationType().getDeclaredMethods(), annoField.toString()),
                                                         elementsRtrnType,
                                                         someClass.getPackage().getName(),
                                                         someClass.getSimpleName());
                                if (annotationDatas != null)
                                    elements.put(annoField.annotationType().getSimpleName(), annotationDatas);		    		  
                            }
	        	}
                    }	 
	        }	  		
            }

	    //Anotacao de Construtor
            Constructor[] construtors = someClass.getDeclaredConstructors();
            if( construtors != null ) {
                for (Constructor c:construtors) {
                    Annotation[] annoConstructors = c.getAnnotations();
                    if (annoConstructors != null) {
                        for (Annotation annoConstructor:annoConstructors) {
                            if (annotationClass == null || annotationClass.getSimpleName().equals(annoConstructor.annotationType().getSimpleName())) {
                                elementsRtrnType = splitMethodRtrnType(annoConstructor.annotationType().getDeclaredMethods());
                                Set<AnnotationData> annotationDatas = getAnnotationDatas(annoConstructor.annotationType().getSimpleName(), 
                                                         annoConstructor.annotationType().getPackage().getName(), 
                                                         c.getName(),
                                                         c.getDeclaringClass(),
                                                         AnnotationType.Constructor, 
                                                         splitAnnotationValue(annoConstructor.annotationType().getDeclaredMethods(), annoConstructor.toString()),
                                                         elementsRtrnType,
                                                         someClass.getPackage().getName(),
                                                         someClass.getSimpleName());                                                         
                                if (annotationDatas != null)
                                    elements.put(annoConstructor.annotationType().getSimpleName(), annotationDatas);		    		  
                            }
	        	}        			  
                    }
	        }	  
            } 

	    //Anotacao de Metodos
	    Method[] methods = someClass.getDeclaredMethods();
            if( methods != null ) {
                for (Method m:methods) {
                    Annotation[] annoMethods = m.getAnnotations();
                    if (annoMethods != null) {
                        for (Annotation annoMethod:annoMethods) {
                            if (annotationClass == null || annotationClass.getSimpleName().equals(annoMethod.annotationType().getSimpleName())) {
                                elementsRtrnType = splitMethodRtrnType(annoMethod.annotationType().getDeclaredMethods());
                                Set<AnnotationData> annotationDatas = getAnnotationDatas(annoMethod.annotationType().getSimpleName(), 
                                                         annoMethod.annotationType().getPackage().getName(), 
                                                         m.getName(),
                                                         m.getReturnType(),
                                                         AnnotationType.Method, 
                                                         splitAnnotationValue(annoMethod.annotationType().getDeclaredMethods(), annoMethod.toString()),
                                                         elementsRtrnType,
                                                         someClass.getPackage().getName(),
                                                         someClass.getSimpleName());                                                         
                                 if (annotationDatas != null)
                                    elements.put(annoMethod.annotationType().getSimpleName(), annotationDatas);		    		  
                             }
                        }        			  
                    }
	       }	  
            }
		      
            annotationElement.setAllAnnotationData(elements);
        }
	catch (Exception e) {
            // TODO: handle exception
	}
	return annotationElement;
    }
	
   /*
    * AnnotationData: representa um elemento que compoem uma anotacao
    * Uma anotacao pode ter um ou mais elementos  
    */
    private static Set<AnnotationData> getAnnotationDatas(String annotationName, String annotationPackage, String annotatedElementName, 
                       Class annotatedElementType, AnnotationType annotationType, Map<String, String> elements, 
                       Map<String, Class> elementsRtrnType, String packageName, String  className) {
        Set<AnnotationData> annotationDatas = null; 	
	if (elements != null) {
            annotationDatas = new HashSet<AnnotationData>();
	    Set<Map.Entry<String, String>> set = elements.entrySet();
            for (Iterator<Map.Entry<String, String>> iter = set.iterator(); iter.hasNext();) {
            	Map.Entry<String, String> me = iter.next();                
        	AnnotationData annotationData = new AnnotationData();
                //Dados Classe anotacao
                annotationData.setAnnotationName(annotationName);
                annotationData.setAnnotationPackage(annotationPackage);
                annotationData.setAnnotationType(annotationType); 
        	annotationData.setAnnotationMember(me.getKey());
        	annotationData.setAnnotationReturnType(elementsRtrnType.get(me.getKey()));
        	annotationData.setAnnotationValue(me.getValue()); 
                //Dados da classe que FOI anotada
                annotationData.setAnnotatedClass(className); 
                annotationData.setAnnotatedPackage(packageName);                
                annotationData.setAnnotatedElementName(annotatedElementName);
                annotationData.setAnnotatedElementType(annotatedElementType);
            }		    	
	}
	return annotationDatas;
    }

   /*
    * Converte valores toString() da anotacao em chave=valor onde:
    * chave: Identifica o elemento chave da anotacao
    * valor: Representa o conteudo associado a esta chave
    * Uma anotacao pode ter um ou mais elementos
    */
    private static Map<String, String> splitAnnotationValue(Method[] methods, String conteudo) {
        Map<String, String> result = null;
        if (methods != null && methods.length > 0) {
            result = new HashMap<String, String>(methods.length);
            StringBuilder sb = new StringBuilder();		
            for (int i=0; i<methods.length; i++) {
                sb.setLength(0);
    		sb.append("(");
    		sb.append(methods[i].getName());
    		sb.append("=.+)");
    		int x = i+1;
    		if (x<methods.length) {
                    sb.append("(?=,\\s");
                    sb.append(methods[x].getName());
                    sb.append("[=].+)");				
    		}
    		else {
                    sb.append("(?=[)]$)");	
    		}
    		Pattern pattern = Pattern.compile(sb.toString());
    		Matcher matcher = pattern.matcher(conteudo);
    		if (matcher.find()) {
                    for (int k=0; k<matcher.groupCount();k++) {
    			String valor = matcher.group(k+1);
                        valor = valor.substring(valor.indexOf("=")+1);
    			result.put(methods[i].getName(),valor);
                    }	
    		}
                else { 
                    result.put(methods[i].getName(),methods[i].getDefaultValue().toString());                              
                }   
            }
        }
	return result;
    }

   /*
    * Retorna o tipo de retorno dos methods
    */
    private static Map<String, Class> splitMethodRtrnType(Method[] methods) {
        Map<String, Class> result = null;
        if (methods != null && methods.length > 0) {
            result = new HashMap<String, Class>(methods.length);
            for (int i=0; i<methods.length; i++) 
                result.put(methods[i].getName(), methods[i].getReturnType());
        }
	return result;
    }

}